// ##########################################################################
// #                                                                        #
// #                              CLOUDCOMPARE                              #
// #                                                                        #
// #  This program is free software; you can redistribute it and/or modify  #
// #  it under the terms of the GNU General Public License as published by  #
// #  the Free Software Foundation; version 2 or later of the License.      #
// #                                                                        #
// #  This program is distributed in the hope that it will be useful,       #
// #  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
// #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the          #
// #  GNU General Public License for more details.                          #
// #                                                                        #
// #          COPYRIGHT: EDF R&D / TELECOM ParisTech (ENST-TSI)             #
// #                                                                        #
// ##########################################################################

#include "ccDish.h"

// Local
#include "ccNormalVectors.h"
#include "ccPointCloud.h"

ccDish::ccDish(PointCoordinateType radius,
               PointCoordinateType height,
               PointCoordinateType radius2 /*=0*/,
               const ccGLMatrix*   transMat /*=nullptr*/,
               QString             name /*="Dish"*/,
               unsigned            precision /*=DEFAULT_DRAWING_PRECISION*/)
    : ccGenericPrimitive(name, transMat)
    , m_baseRadius(radius)
    , m_secondRadius(radius2)
    , m_height(height)
{
	if (radius2 == 0)
		m_height = std::min(height, radius);                                   // in spherical mode, the height can't be superior to the radius! (dishes are at most hemispheres)
	setDrawingPrecision(std::max<unsigned>(precision, MIN_DRAWING_PRECISION)); // automatically calls buildUp + 	applyTransformationToVertices
}

ccDish::ccDish(QString name /*="Sphere"*/)
    : ccGenericPrimitive(name)
    , m_baseRadius(0)
    , m_secondRadius(0)
    , m_height(0)
{
}

ccGenericPrimitive* ccDish::clone() const
{
	return finishCloneJob(new ccDish(m_baseRadius, m_height, m_secondRadius, &m_transformation, getName(), m_drawPrecision));
}

bool ccDish::buildUp()
{
	if (m_drawPrecision < MIN_DRAWING_PRECISION)
		return false;

	if (m_height <= 0 || m_baseRadius <= 0 || m_secondRadius < 0) // invalid parameters
		return false;

	// section angular span
	double       startAngle_rad = 0.0;
	const double endAngle_rad   = M_PI / 2.0;

	PointCoordinateType realRadius = m_baseRadius;
	if (m_secondRadius == 0 && m_height < m_baseRadius) // partial spherical mode
	{
		realRadius     = (m_height * m_height + m_baseRadius * m_baseRadius) / (2 * m_height);
		startAngle_rad = acos(m_baseRadius / realRadius);
		assert(startAngle_rad < endAngle_rad);
	}

	const unsigned steps                = m_drawPrecision;
	double         angleStep_rad        = 2.0 * M_PI / steps;
	unsigned       sectionSteps         = static_cast<unsigned>(ceil((endAngle_rad - startAngle_rad) * m_drawPrecision / (2.0 * M_PI)));
	double         sectionAngleStep_rad = (endAngle_rad - startAngle_rad) / sectionSteps;

	// vertices
	unsigned vertCount = steps * sectionSteps + 1; //+1 for north pole
	// faces
	unsigned faceCount = steps * ((sectionSteps - 1) * 2 + 1);

	if (!init(vertCount, true, faceCount, 0))
	{
		ccLog::Error("[ccDish::buildUp] Not enough memory");
		return false;
	}

	// vertices
	ccPointCloud* verts = vertices();
	assert(verts);

	// first point: north pole
	verts->addPoint(CCVector3(0, 0, m_height));
	verts->addNorm(CCVector3(0, 0, 1));

	// then, angular sweep
	{
		for (unsigned j = 1; j <= sectionSteps; ++j)
		{
			PointCoordinateType theta     = static_cast<PointCoordinateType>(endAngle_rad - j * sectionAngleStep_rad); // we start from north pole!
			PointCoordinateType cos_theta = cos(theta);
			PointCoordinateType sin_theta = sin(theta);

			CCVector3 N0(cos_theta, 0, sin_theta);

			for (unsigned i = 0; i < steps; ++i) // then we make a full revolution
			{
				PointCoordinateType phi     = static_cast<PointCoordinateType>(i * angleStep_rad);
				PointCoordinateType cos_phi = cos(phi);
				PointCoordinateType sin_phi = sin(phi);

				CCVector3 N(N0.x * cos_phi, N0.x * sin_phi, N0.z);
				N.normalize();

				CCVector3 P = N * realRadius;

				if (m_secondRadius > 0) // half-ellipsoid mode
				{
					P.y *= (m_secondRadius / m_baseRadius);
					P.z *= (m_height / m_baseRadius);
				}
				else // spherical section mode
				{
					P.z += m_height - realRadius;
				}

				verts->addPoint(P);
				verts->addNorm(N);
			}
		}
	}

	// faces
	{
		// north pole
		{
			for (unsigned i = 0; i < steps; ++i)
			{
				unsigned A = 1 + i;
				unsigned B = (i + 1 < steps ? A + 1 : 1);
				addTriangle(A, B, 0);
			}
		}

		// slices
		for (unsigned j = 1; j < sectionSteps; ++j)
		{
			unsigned shift = 1 + (j - 1) * steps;
			for (unsigned i = 0; i < steps; ++i)
			{
				unsigned A = shift + i;
				unsigned B = (i + 1 < steps ? A + 1 : shift);
				assert(B < vertCount);
				addTriangle(A, A + steps, B);
				addTriangle(B + steps, B, A + steps);
			}
		}
	}

	notifyGeometryUpdate();
	showNormals(true);

	return true;
}

bool ccDish::toFile_MeOnly(QFile& out, short dataVersion) const
{
	assert(out.isOpen() && (out.openMode() & QIODevice::WriteOnly));
	if (dataVersion < 21)
	{
		assert(false);
		return false;
	}

	if (!ccGenericPrimitive::toFile_MeOnly(out, dataVersion))
	{
		return false;
	}

	// parameters (dataVersion>=21)
	QDataStream outStream(&out);
	outStream << m_baseRadius;
	outStream << m_secondRadius;
	outStream << m_height;

	return true;
}

bool ccDish::fromFile_MeOnly(QFile& in, short dataVersion, int flags, LoadedIDMap& oldToNewIDMap)
{
	if (!ccGenericPrimitive::fromFile_MeOnly(in, dataVersion, flags, oldToNewIDMap))
		return false;

	// parameters (dataVersion>=21)
	QDataStream inStream(&in);
	ccSerializationHelper::CoordsFromDataStream(inStream, flags, &m_baseRadius);
	ccSerializationHelper::CoordsFromDataStream(inStream, flags, &m_secondRadius);
	ccSerializationHelper::CoordsFromDataStream(inStream, flags, &m_height);

	return true;
}

short ccDish::minimumFileVersion_MeOnly() const
{
	return std::max(static_cast<short>(21), ccGenericPrimitive::minimumFileVersion_MeOnly());
}
